import {Command, Option, UsageError}                    from 'clipanion';
import fs                                               from 'fs';
import path                                             from 'path';

import * as folderUtils                                 from '../folderUtils';
import * as specUtils                                   from '../specUtils';
import {Descriptor, Locator, isSupportedPackageManager} from '../types';

import {BaseCommand}                                    from './Base';

export class InstallGlobalCommand extends BaseCommand {
  static paths = [
    [`install`],
  ];

  static usage = Command.Usage({
    description: `Install package managers on the system`,
    details: `
      Download the selected package managers and install them on the system.

      Package managers thus installed will be configured as the new default when calling their respective binaries outside of projects defining the 'packageManager' field.
    `,
    examples: [[
      `Install the latest version of Yarn 1.x and make it globally available`,
      `corepack install -g yarn@^1`,
    ], [
      `Install the latest version of pnpm, and make it globally available`,
      `corepack install -g pnpm`,
    ]],
  });

  global = Option.Boolean(`-g,--global`, {
    required: true,
  });

  cacheOnly = Option.Boolean(`--cache-only`, false, {
    description: `If true, the package managers will only be cached, not set as new defaults`,
  });

  args = Option.Rest();

  async execute() {
    if (this.args.length === 0)
      throw new UsageError(`No package managers specified`);

    await Promise.all(this.args.map(arg => {
      if (arg.endsWith(`.tgz`)) {
        return this.installFromTarball(path.resolve(this.context.cwd, arg));
      } else {
        return this.installFromDescriptor(specUtils.parseSpec(arg, `CLI arguments`, {enforceExactVersion: false}));
      }
    }));
  }

  log(locator: Locator) {
    if (this.cacheOnly) {
      this.context.stdout.write(`Adding ${locator.name}@${locator.reference} to the cache...\n`);
    } else {
      this.context.stdout.write(`Installing ${locator.name}@${locator.reference}...\n`);
    }
  }

  async installFromDescriptor(descriptor: Descriptor) {
    const resolved = await this.context.engine.resolveDescriptor(descriptor, {allowTags: true, useCache: false});
    if (resolved === null)
      throw new UsageError(`Failed to successfully resolve '${descriptor.range}' to a valid ${descriptor.name} release`);

    this.log(resolved);
    await this.context.engine.ensurePackageManager(resolved);

    if (!this.cacheOnly) {
      await this.context.engine.activatePackageManager(resolved);
    }
  }

  async installFromTarball(p: string) {
    const installFolder = folderUtils.getInstallFolder();

    const archiveEntries = new Map<string, Set<string>>();
    const {default: tar} = await import(`tar`);

    let hasShortEntries = false;

    await tar.t({file: p, onentry: entry => {
      const segments = entry.path.split(/\//g);
      if (segments.length > 0 && segments[segments.length - 1] !== `.corepack`)
        return;

      if (segments.length < 3) {
        hasShortEntries = true;
      } else {
        let references = archiveEntries.get(segments[0]);
        if (typeof references === `undefined`)
          archiveEntries.set(segments[0], references = new Set());

        references.add(segments[1]);
      }
    }});

    if (hasShortEntries || archiveEntries.size < 1)
      throw new UsageError(`Invalid archive format; did it get generated by 'corepack pack'?`);

    for (const [name, references] of archiveEntries) {
      for (const reference of references) {
        if (!isSupportedPackageManager(name))
          throw new UsageError(`Unsupported package manager '${name}'`);

        this.log({name, reference});

        // Recreate the folder in case it was deleted somewhere else:
        await fs.promises.mkdir(installFolder, {recursive: true});

        await tar.x({file: p, cwd: installFolder}, [`${name}/${reference}`]);

        if (!this.cacheOnly) {
          await this.context.engine.activatePackageManager({name, reference});
        }
      }
    }
  }
}
